package org.finjic.factory;

import org.finjic.entity.Argument;
import org.finjic.entity.BeanDefinition;
import org.finjic.entity.ConstructorArg;
import org.finjic.exception.NoSuchBeanException;
import org.finjic.type.ArgumentType;
import org.finjic.utils.AppUtils;
import org.finjic.utils.CollectionUtils;
import org.finjic.utils.ReflectionUtils;
import org.finjic.utils.StringUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * User: Taras
 * Date: 04.11.12
 * Time: 19:25
 */
public class BeanFactoryImpl implements BeanFactory {
    Set<BeanDefinition> beanDefinitions;
    Map<String, Object> beans = new HashMap<String, Object>();

    public BeanFactoryImpl(Set<BeanDefinition> beanDefinitions) {
        this.beanDefinitions = beanDefinitions;
        processDefinitions();
    }

    private void processDefinitions() {
        for (BeanDefinition beanDefinition : beanDefinitions) {
            if (!beans.containsKey(beanDefinition.getId())) {
                Object bean = processDefinition(beanDefinition);
                beans.put(beanDefinition.getId(), bean);
            }
        }
    }

    private Object processDefinition(BeanDefinition beanDefinition) {
        String beanId = beanDefinition.getId();
        if (beanIsAlreadyCreated(beanId)) {
            return getBean(beanId);
        }
        Class clazz = getBeanClass(beanDefinition);
        Map<Integer, Object> constructorArguments = getConstructorArguments(beanDefinition);
        Map<String, Object> beanProperties = getBeanProperties(beanDefinition);
        return createBean(clazz, constructorArguments, beanProperties);
    }

    private boolean beanIsAlreadyCreated(String beanId) {
        return beans.keySet().contains(beanId);
    }

    private Class getBeanClass(BeanDefinition beanDefinition) {
        Class clazz;
        try {
            clazz = Class.forName(beanDefinition.getType());
        } catch (Exception e) {
            throw new RuntimeException(e.getCause());
        }
        return clazz;
    }

    private Map<String, Object> getBeanProperties(BeanDefinition beanDefinition) {
        if (CollectionUtils.isEmpty(beanDefinition.getProperties())) {
            return Collections.emptyMap();
        }
        Map<String, Object> properties = new HashMap<String, Object>();
        for (Map.Entry<String, Argument> entry : beanDefinition.getProperties().entrySet()) {
            Object arg = processArgument(entry.getValue());
            properties.put(entry.getKey(), arg);
        }
        return properties;
    }

    private Map<Integer, Object> getConstructorArguments(BeanDefinition beanDefinition) {
        if (CollectionUtils.isEmpty(beanDefinition.getConstructorArgs())) {
            return Collections.emptyMap();
        }
        Map<Integer, Object> args = new HashMap<Integer, Object>();
        for (ConstructorArg cArg : beanDefinition.getConstructorArgs()) {
            Object arg = processArgument(cArg);
            args.put(cArg.getIndex(), arg);
        }
        return args;
    }

    private Object processArgument(Argument argument) {
        if (argument == null) {
            return null;
        }
        ArgumentType type = argument.getType();
        Object value = argument.getValue();
        Object result;
        if (type == ArgumentType.REFERENCE) {
            BeanDefinition argBeanDefinition = AppUtils.getBeanDefinitionById((String) value, beanDefinitions);
            result = processDefinition(argBeanDefinition);
        } else if (type == ArgumentType.OBJECT) {
            result = value;
        } else {
            throw new RuntimeException("Unknown property type: " + type);
        }
        return result;
    }

    private Object createBean(Class clazz, Map<Integer, Object> constructorArgs, Map<String, Object> properties) {
        Object bean = createObject(clazz, constructorArgs);
        initObject(bean, properties);
        return bean;
    }

    private Object createObject(Class clazz, Map<Integer, Object> args) {
        Object[] constructorArgs = new Object[args.size()];
        Class[] constructorArgsClasses = new Class[args.size()];
        for (Integer position : args.keySet()) {
            Object arg = args.get(position);
            constructorArgs[position] = arg;
            constructorArgsClasses[position] = arg.getClass();
        }
        Constructor constructor = getConstructor(clazz, constructorArgsClasses);
        if (constructor == null) {
            return null;
        }
        Object obj = null;
        try {
            obj = constructor.newInstance(constructorArgs);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

    @SuppressWarnings("unchecked")
    private Constructor getConstructor(Class clazz, Class[] constructorArgsClasses) {
        if (!clazz.isInterface()) {
            Constructor constructor = null;
            try {
                constructor = clazz.getConstructor(constructorArgsClasses);
            } catch (Exception e) {
                //these parameters doesn't match any constructor from this class
            }
            return constructor;
        } else {
            Set<Class> subTypes = ReflectionUtils.getSubclasses("org.finjic", clazz);
            for (Class subType : subTypes) {
                Constructor constructor = getConstructor(subType, constructorArgsClasses);
                if (constructor != null) {
                    return constructor;
                }
            }
        }
        return null;
    }

    private void initObject(Object bean, Map<String, Object> properties) {
        for (Map.Entry<String, Object> property : properties.entrySet()) {
            Method method = getSetter(bean, property.getKey(), property.getValue().getClass());
            if (method != null) {
                try {
                    method.invoke(bean, property.getValue());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Method getSetter(Object bean, String propertyName, Class propertyType) {
        String methodName = StringUtils.beanMethodName("set", propertyName);
        Method method = null;
        try {
            method = bean.getClass().getMethod(methodName, propertyType);
        } catch (Exception e) {
            //no setter method for this property
        }
        return method;
    }

    @Override
    public Object getBean(String id) {
        if (!beans.containsKey(id)) {
            throw new NoSuchBeanException(id);
        }
        return beans.get(id);
    }
}
